Skip to content

Support colocated joins when a partition holds no segments - #19166

Merged
yashmayya merged 2 commits into
apache:masterfrom
yashmayya:colocated-join-empty-partitions
Aug 18, 2026
Merged

Support colocated joins when a partition holds no segments#19166
yashmayya merged 2 commits into
apache:masterfrom
yashmayya:colocated-join-empty-partitions

Conversation

@yashmayya

@yashmayya yashmayya commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Why

A colocated join fails today when a partition of one of its tables holds no segments:

Failed to find any segment for table: myTable_OFFLINE, partition: 3

This is easy to hit and hard to avoid:

  • A partition key with skew leaves some partitions with no rows.
  • Retention removes every segment of a partition.
  • A stream partition exists but has never committed a segment.
  • One logical partition space is split across several tables, so each table populates a subset.
  • A table declares more partitions than it currently uses, to leave room to grow.

The assignment cannot simply skip the empty partition. Worker ids come from a counter over the partitions that hold data, so skipping one shifts every later partition down a slot. Two tables that each drop a different empty partition then end up with equal worker counts, get wired one-to-one, and join mismatched partitions. That loses rows and reports no error, which is why the assignment refuses to continue at all.

What changes

  • The stages tied together by direct exchanges form a group, and the group shares one ordered list of partition classes.
  • A class leaves the list only when no member of the group holds data in it.
  • A class the group keeps, but one member holds no data for, gets a worker with no segments. That worker sits on a server borrowed from a member that does hold the class, so the exchange stays in process.
  • Both sides of every direct exchange assert that they agree on the list. A disagreement fails at plan time instead of returning wrong rows.
  • The broker now publishes the partitions whose only segments are new and have no online replica. Those hold data that no server can serve as a whole, so they keep failing.

A side effect is less fan-out. A table that declares 8 partitions and populates 3 runs 3 workers, so the broker sends fewer requests and waits on fewer servers.

What now fails on purpose

  • A hybrid table with segments whose partition metadata does not match the table config now fails planning. It used to answer and drop those segments in silence. The most common cause is a partition config added after some segments were built.
  • A table spread over more than one cluster reports no partition info, so a partition-aware plan is not attempted on a partial view. No server holds another cluster's segments, so one cluster's view would make a partition served elsewhere look empty.

Broker pruning comes next

Broker pruning is off for a colocated join today, and this change is what makes it possible.

  • Pruning drops partitions that hold no matching segment. That is the same operation as dropping a class that holds no data.
  • The group already shares one class list, so a pruned class can leave the list the same way an empty one does, and both sides stay in step.
  • The gain is fewer servers per query, which protects tail latency on large clusters.

A follow-up PR adds it.

Testing

  • 19 files. Planner, runtime and broker suites all pass.
  • An end-to-end test joins two tables that declare 8 partitions and populate 3 and 4 of them. It asserts the rows, the worker count per leaf, the segments each leaf reads, and that each send goes to one receiver with the same worker id. That last check proves the plan did not fall back to a shuffle.
  • One test drives the broker metadata directly and proves the new signal does not depend on map iteration order.

Three things left out on purpose

Aggregation merge identity is not covered here. A leaf that scans nothing emits one identity row for an aggregation with no GROUP BY. That predates this change: a worker whose segments are all pruned on the server already does the same. This change raises how many such rows reach the merge without introducing the dependency. The one aggregation that is not a true merge identity, PERCENTILEKLL, fails only when every worker is empty, which this change does not newly reach. It is tracked separately.

A worker with no segments is charged against the query thread estimate like any other worker. It is dispatched and does run a leaf operator. The estimate over-counts by two threads per such worker. That is conservative, and it only affects colocated joins over a partition space that is largely unpopulated. Those queries failed outright before, so there is no earlier estimate to compare against.

A partitioned table outside a colocated join still fails on an empty partition. A shuffled send puts no constraint on its worker ids, so the leaf joins no group and gets no class list. The error is the same one this change removes for colocated joins, and it predates this change.

Labels

Applied: bug, backward-incompat, release-notes, multi-stage, query

@yashmayya yashmayya added bug Something is not working as expected backward-incompat Introduces a backward-incompatible API or behavior change release-notes Referenced by PRs that need attention when compiling the next release notes multi-stage Related to the multi-stage query engine query Related to query processing labels Aug 5, 2026
@codecov-commenter

codecov-commenter commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.86047% with 35 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.17%. Comparing base (6c8f8c0) to head (5526d81).

Files with missing lines Patch % Lines
.../org/apache/pinot/query/routing/WorkerManager.java 90.21% 4 Missing and 19 partials ⚠️
...e/routing/TablePartitionReplicatedServersInfo.java 42.85% 3 Missing and 1 partial ⚠️
...e/pinot/query/routing/ColocationGroupAnalyzer.java 96.33% 0 Missing and 4 partials ⚠️
...apache/pinot/query/routing/LeafPartitionHints.java 93.10% 0 Missing and 2 partials ⚠️
...mentpartition/SegmentPartitionMetadataManager.java 91.66% 0 Missing and 1 partial ⚠️
...ery/planner/physical/MailboxAssignmentVisitor.java 90.90% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             master   #19166       +/-   ##
=============================================
+ Coverage      0.00%   67.17%   +67.17%     
- Complexity        0     1424     +1424     
=============================================
  Files             3     3462     +3459     
  Lines             6   220358   +220352     
  Branches          0    35146    +35146     
=============================================
+ Hits              0   148030   +148030     
- Misses            6    60509    +60503     
- Partials          0    11819    +11819     
Flag Coverage Δ
integration 100.00% <ø> (+100.00%) ⬆️
integration1 100.00% <ø> (?)
integration2 0.00% <ø> (ø)
java-25 67.17% <91.86%> (+67.17%) ⬆️
lane-a 100.00% <ø> (+100.00%) ⬆️
lane-b 0.00% <ø> (ø)
temurin 67.17% <91.86%> (+67.17%) ⬆️
unittests 67.17% <91.86%> (?)
unittests1 57.82% <91.54%> (?)
unittests2 39.20% <25.58%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Enables colocated joins to handle empty partitions while preserving partition-to-worker alignment and reducing fan-out.

Changes:

  • Adds shared partition-class assignment and empty-worker padding.
  • Adds deferred-partition and multi-cluster safeguards.
  • Expands planner, broker, mailbox, and integration coverage.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
pinot-query-planner/.../WorkerManagerTest.java Covers partition assignment and padding.
pinot-query-planner/.../ColocationGroupAnalyzerTest.java Tests reducible group detection.
pinot-query-planner/.../QueryEnvironmentTestBase.java Updates partition metadata construction.
pinot-query-planner/.../PinotDispatchPlannerTest.java Tests spooled-stage assignment.
pinot-query-planner/.../MailboxAssignmentVisitorTest.java Tests direct-exchange class agreement.
pinot-query-planner/.../DispatchableSubPlanTest.java Tests segment-map preservation.
pinot-query-planner/.../WorkerManager.java Implements class reduction and padding.
pinot-query-planner/.../LeafPartitionHints.java Centralizes partition-hint parsing.
pinot-query-planner/.../ColocationGroupAnalyzer.java Identifies colocated fragment groups.
pinot-query-planner/.../MailboxAssignmentVisitor.java Validates direct-exchange mappings.
pinot-query-planner/.../DispatchablePlanMetadata.java Stores broker-local class metadata.
pinot-query-planner/.../DispatchablePlanFragment.java Preserves segment assignments when copied.
pinot-query-planner/.../DispatchablePlanContext.java Caches partition metadata per query.
pinot-integration-tests/.../ColocatedJoinEmptyPartitionTest.java Adds end-to-end join coverage.
pinot-core/.../TablePartitionReplicatedServersInfo.java Publishes deferred partitions.
pinot-broker/.../SegmentPartitionMetadataManagerTest.java Tests deferred-partition detection.
pinot-broker/.../MultiClusterRoutingManagerTest.java Tests cross-cluster partition metadata.
pinot-broker/.../SegmentPartitionMetadataManager.java Tracks deferred-only partitions.
pinot-broker/.../MultiClusterRoutingManager.java Avoids combining partition arrays.
Suppressed comments (2)

pinot-broker/src/main/java/org/apache/pinot/broker/routing/manager/MultiClusterRoutingManager.java:239

  • A failed remote lookup leaves it unknown whether that cluster also serves the table. Continuing can return another cluster's partition array and enable a partition-aware plan over a partial view, which can silently drop remote-only partitions. Return null on this failure so the caller falls back instead.
      } catch (Exception e) {
        LOGGER.error("Error getting table partition info from remote cluster routing manager for table {}",
            tableNameWithType, e);
        continue;

pinot-broker/src/main/java/org/apache/pinot/broker/routing/manager/MultiClusterRoutingManager.java:243

  • A null partition-info response does not mean this cluster lacks the table; a routing entry can exist without usable partition metadata. Treating that cluster as absent can return the local array for a table spread across clusters and silently omit its partitions. Check routingExists and return null when the remote cluster serves the table.
      if (remotePartitionInfo == null) {
        continue;
      }

Comment on lines +1625 to +1626
partitionsWithOnlyDeferredSegments.removeIf(
partitionId -> partitionId < partitionInfoMap.length && partitionInfoMap[partitionId] != null);
Comment on lines +230 to +231
TablePartitionReplicatedServersInfo partitionInfo =
_localClusterRoutingManager.getTablePartitionReplicatedServersInfo(tableNameWithType);
@gortiz
gortiz self-requested a review August 7, 2026 10:23
@yashmayya
yashmayya force-pushed the colocated-join-empty-partitions branch from 5a48ddb to 45447e7 Compare August 7, 2026 21:39
@yashmayya
yashmayya requested a review from Jackie-Jiang August 7, 2026 22:14

@gortiz gortiz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great contribution!

A colocated join failed outright when a partition of one of its tables held
no segments, with "Failed to find any segment for table: X, partition: N".
Worker ids came from a running counter over the partitions that held data, so
skipping an empty one would shift every later partition down a slot. Two
tables each dropping a different empty partition could then end up with equal
worker counts and be wired 1-to-1 onto mismatched partitions, losing rows with
no error, which is why the assignment refused to continue at all.

The stages tied together by direct exchanges now share one ordered list of
partition classes, dropping only the classes that hold no data on any member.
A class the group keeps but a member holds no data for gets a worker with no
segments, placed on a server borrowed from a member that does hold that class
so the exchange stays in process. The two sides of every direct exchange
assert that they agree on the list.

The broker publishes the partitions whose only segments are new and have no
online replica. Those hold data that no server can serve as a whole, so they
keep failing rather than being read as empty.

A worker with no segments is charged against the query thread estimate like
any other worker: it is dispatched and does run a leaf operator. The estimate
therefore over-counts by two threads per such worker, which is conservative
and only affects colocated joins over a partition space that is largely
unpopulated. Those queries failed outright before, so there is no earlier
estimate to compare against.

Aggregation merge identity is deliberately not covered here. A leaf that
scans nothing emits one identity row for an aggregation with no GROUP BY, but
that predates this change: a worker whose segments are all pruned on the
server already does the same. Padding raises how many such rows reach the
merge without introducing the dependency, and the one aggregation that is not
a true merge identity fails only when every worker is empty, which this change
does not newly reach.
@yashmayya
yashmayya force-pushed the colocated-join-empty-partitions branch from 39da748 to 5526d81 Compare August 18, 2026 21:20
@yashmayya
yashmayya merged commit f916ae5 into apache:master Aug 18, 2026
12 checks passed
xiangfu0 added a commit to pinot-contrib/pinot-docs that referenced this pull request Aug 18, 2026
## Summary

- document colocated joins when one table has an empty partition
- explain shared partition-class alignment and reduced worker fan-out
- call out partition-metadata validation and remaining failure cases

## Upstream context

Follows apache/pinot#19166.

## Validation

- `git diff --check`

Co-authored-by: Xiang Fu <xiangfu@Xiang-mac-mtv-2.local>
@xiangfu0

Copy link
Copy Markdown
Contributor

The follow-up documentation is available in pinot-contrib/pinot-docs#996: pinot-contrib/pinot-docs#996

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backward-incompat Introduces a backward-incompatible API or behavior change bug Something is not working as expected multi-stage Related to the multi-stage query engine query Related to query processing release-notes Referenced by PRs that need attention when compiling the next release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants